summaryrefslogtreecommitdiff
path: root/ui/routes/(app)/c/[conversation]/+page.svelte
diff options
context:
space:
mode:
authorOwen Jacobson <owen@grimoire.ca>2025-07-01 15:30:44 -0400
committerOwen Jacobson <owen@grimoire.ca>2025-07-03 22:43:44 -0400
commit1cafeb5ec92c1dc4ad74fbed58b15a8ab2f3c0cf (patch)
tree0ecca3ed4133210286e9d11b2d2027136f09113a /ui/routes/(app)/c/[conversation]/+page.svelte
parent8d412732dc094ead3c5cf86c005d187f9624fc65 (diff)
Move the `/ch` channel view to `/c` (for conversation).
Diffstat (limited to 'ui/routes/(app)/c/[conversation]/+page.svelte')
-rw-r--r--ui/routes/(app)/c/[conversation]/+page.svelte117
1 files changed, 117 insertions, 0 deletions
diff --git a/ui/routes/(app)/c/[conversation]/+page.svelte b/ui/routes/(app)/c/[conversation]/+page.svelte
new file mode 100644
index 0000000..4d2cc86
--- /dev/null
+++ b/ui/routes/(app)/c/[conversation]/+page.svelte
@@ -0,0 +1,117 @@
+<script>
+ import { DateTime } from 'luxon';
+ import { page } from '$app/state';
+ import MessageInput from '$lib/components/MessageInput.svelte';
+ import MessageRun from '$lib/components/MessageRun.svelte';
+ import Message from '$lib/components/Message.svelte';
+ import { runs } from '$lib/runs.js';
+
+ const { data } = $props();
+ const { session, outbox } = data;
+ let activeChannel;
+
+ const channelId = $derived(page.params.conversation);
+ const channel = $derived(session.channels.find((channel) => channel.id === channelId));
+ const messages = $derived(
+ session.messages.filter((message) => message.conversation === channelId),
+ );
+ const unsent = $derived(outbox.messages.filter((message) => message.channel === channelId));
+ const deleted = $derived(outbox.deleted.map((message) => message.messageId));
+ const unsentSkeletons = $derived(
+ unsent.map((message) => message.toSkeleton($state.snapshot(session.currentUser))),
+ );
+ const messageRuns = $derived(runs(messages.concat(unsentSkeletons), session.currentUser));
+
+ function inView(parentElement, element) {
+ const parRect = parentElement.getBoundingClientRect();
+ const parentTop = parRect.top;
+ const parentBottom = parRect.bottom;
+
+ const elRect = element.getBoundingClientRect();
+ const elementTop = elRect.top;
+ const elementBottom = elRect.bottom;
+
+ return parentTop < elementTop && parentBottom > elementBottom;
+ }
+
+ function getLastVisibleMessage() {
+ if (activeChannel) {
+ const childElements = activeChannel.getElementsByClassName('message');
+ const lastInView = Array.from(childElements)
+ .reverse()
+ .find((el) => {
+ return inView(activeChannel, el);
+ });
+ return lastInView;
+ }
+ }
+
+ function setLastRead() {
+ const lastInView = getLastVisibleMessage();
+ const at = !!lastInView ? DateTime.fromISO(lastInView.dataset.at) : channel?.at;
+ if (!!at) {
+ session.local.updateLastReadAt(channelId, at);
+ }
+ }
+
+ $effect(() => {
+ const _ = session.messages;
+ setLastRead();
+ });
+
+ $effect(() => {
+ // This is just to force it to track messageRuns.
+ const _ = messageRuns;
+ document.querySelector('.message-run:last-child .message:last-child')?.scrollIntoView();
+ });
+
+ function handleKeydown(event) {
+ if (event.key === 'Escape') {
+ setLastRead(); // TODO: pass in "last message DT"?
+ }
+ }
+
+ let lastReadCallback = null;
+
+ function onscroll() {
+ clearTimeout(lastReadCallback); // Fine if lastReadCallback is null still.
+ lastReadCallback = setTimeout(setLastRead, 2 * 1000);
+ }
+
+ async function sendMessage(message) {
+ outbox.postToChannel(channelId, message);
+ }
+
+ async function deleteMessage(id) {
+ outbox.deleteMessage(id);
+ }
+</script>
+
+<svelte:window onkeydown={handleKeydown} />
+
+<div class="active-channel" {onscroll} bind:this={activeChannel}>
+ {#each messageRuns as { sender, ownMessage, messages }}
+ <MessageRun
+ {sender}
+ class={{
+ ['own-message']: ownMessage,
+ ['other-message']: !ownMessage,
+ }}
+ >
+ {#each messages as message}
+ <Message
+ {...message}
+ editable={ownMessage}
+ {deleteMessage}
+ class={{
+ unsent: !message.id,
+ deleted: deleted.includes(message.id),
+ }}
+ />
+ {/each}
+ </MessageRun>
+ {/each}
+</div>
+<div class="create-message">
+ <MessageInput {sendMessage} />
+</div>